﻿using System;
using System.Configuration;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Security;
using System.Text;
using NBusiness.Data.Rules;
using NBusiness.Utilities;
using NBusiness.Data.Rules.Validation;
using System.Threading;
using NBusiness.Query;
using System.Data;

namespace NBusiness.Data
{
    [Serializable]
    public abstract class ModelBase : INotifyPropertyChanged
    {
        #region Status Properties
        private bool _isNew = true;
        private bool _isDirty = false;
        private bool _isDeleted = false;
        private bool _isLoading = false;
        
        /// <summary>
        /// If true IsNew indicates that this object has not yet been inserted
        /// into the database.
        /// </summary>
        [Bindable(false)]
        public bool IsNew
        {
            get { return _isNew; }
            private set
            {
                if (_isNew != value)
                {
                    _isNew = value;
                    OnPropertyChanged("IsNew");
                }
            }
        }

        public void MarkOld()
        {
            IsNew = false;
        }

        /// <summary>
        /// If true IsDirty indicates that some property has been altered and
        /// this object needs to be saved to persist those changes.
        /// </summary>
        [Bindable(false)]
        public bool IsDirty
        {
            get { return _isDirty; }
            private set
            {
                if (_isDirty != value)
                {
                    _isDirty = value;
                    OnPropertyChanged("IsDirty");
                }
            }
        }
        public void MarkDirty()
        {
            if (!_isLoading)
            {
                _isDirty = true;
                OnPropertyChanged("IsDirty");
            }
        }

        /// <summary>
        /// If true IsDeleted indicates that this object has been flagged for
        /// delete and should be Saved to be deleted from the database.
        /// </summary>
        [Bindable(false)]
        public bool IsDeleted
        {
            get { return _isDeleted; }
            set
            {
                if (_isDeleted != value)
                {
                    _isDeleted = value;
                    OnPropertyChanged("IsDeleted");
                }
            }
        }

        /// <summary>
        /// When a model is loaded asynchronously you can bind to this property
        /// to determine when it
        /// </summary>
        [Bindable(false)]
        public bool IsLoading
        {
            get { return _isLoading; }
            set
            {
                if (_isLoading != value)
                {
                    _isLoading = value;
                    OnPropertyChanged("IsLoading");
                }
            }
        }

        /// <summary>
        /// May be marked as read only to prevent Persisting changes.
        /// </summary>
        public virtual bool IsReadOnly
        {
            get { return false; }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) 
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        #region Constructors

        /// <summary>
        /// Initializes base class and fetches Authorization Rules. 
        /// </summary>
        public ModelBase()
        {
            Initialize();
        }

        #endregion

        #region Initialize

        /// <summary>
        /// Implement in partial class to allow user code to attach events and implement custom code.
        /// </summary>
        public virtual void Initialize()
        {
        }
        #endregion

        #region Save / Persist

        /// <summary>
        /// Call save on the object to persist its current state and all children
        /// in the database.
        /// </summary>
        public virtual void Save()
        {
            if (IsReadOnly)
                throw new NotSupportedException("Unable to Save a Model with IsReadOnly set to true");

            IQuery query = Provider.Query();
            Persist(query, PersistType.Downward);
            Provider.Execute(query);
        }

        [NonSerialized]
        private PersistenceGuard _persistenceGuard;

        /// <summary>
        /// Persists current state of object with the database. Makes calls Persisted
        /// and DeleteChildren where appropriate.
        /// </summary>
        /// <param name="cn"></param>
        protected internal virtual void Persist(IQuery query, PersistType type)
        {
            if (!IsReadOnly && (_persistenceGuard == null || !_persistenceGuard.Persisting))
            {
                // The persistence guard prevents an object from being persisted more than once by
                // its relationships. It is done this way so that if there is an exception the
                // persistence guard will be turned off when Dispose is called. Otherwise an object
                // would no longer be able to persist for its lifetime.
                using (_persistenceGuard = new PersistenceGuard())
                {
                    // TODO: Cancel an insert/update/delete? You may have to throw an exception...

                    //Always persist upwards
                    OnPersisting(query);
                    if (IsNew && !IsDeleted)
                    {
                        if (!CanInsert) throw new SecurityException("Resources.PersistNotAuthorizedToInsert"); //TODO: add
                        if (!IsValid) throw new ValidationException(this, BrokenRules, "Resources.PersistValidationExceptionMessage");

                        if (!OnInserting())
                        {
                            InsertModel(query);

                            //this must be set before OnPersisted so children don't try to insert a second time.
                            IsNew = false;
                            IsDirty = false;
                            OnInserted();
                        }
                    }
                    else if (IsDeleted)
                    {
                        if (!CanDelete) throw new SecurityException("Resources.PersistNotAuthorizedToDelete");

                        if (!OnDeleting())
                        {
                            DeleteModel(query);
                            OnDeleted();
                            IsNew = true;
                        }
                    }
                    else if (IsDirty)
                    {
                        if (!CanUpdate) throw new SecurityException("Resources.PersistNotAuthorizedToUpdate");
                        if (!IsValid) throw new ValidationException(this, BrokenRules, "Resources.PersistValidationExceptionMessage");

                        if (!OnUpdating())
                        {
                            UpdateModel(query);
                            OnUpdated();
                            IsDirty = false;
                        }
                    }
                    //Only persist downwards as appropriate.
                    if (type == PersistType.Downward) 
                        OnPersisted(query);
                }
            }
        }

        public event EventHandler<PersistEventArgs> Persisting;

        /// <summary>
        /// Call persist on all Parents.
        /// </summary>
        /// <param name="cn"></param>
        protected virtual void OnPersisting(IQuery query)
        {
            if (Persisting != null) 
                Persisting(this, new PersistEventArgs(query));
        }

        /// <summary>
        /// Persiste all children in the handler of this event.
        /// </summary>
        public event EventHandler<PersistEventArgs> Persisted;

        /// <summary>
        /// Call persist on all Children.
        /// </summary>
        /// <param name="cn"></param>
        protected virtual void OnPersisted(IQuery query)
        {
            if (Persisted != null) 
                Persisted(this, new PersistEventArgs(query, PersistType.Downward));
        }
        #endregion

        #region Insert

        /// <summary>
        /// Cannot call Insert directly, insert can only be done as a result of a call to Save();
        /// </summary>
        /// <param name="cn"></param>
        protected abstract void InsertModel(IQuery query);

        public event EventHandler<InsertingEventArgs> Inserting;
        protected virtual bool OnInserting()
        {
            bool cancel = false;
            if (Inserting != null)
            {
                InsertingEventArgs args = new InsertingEventArgs();
                Inserting(this, args);
                cancel = args.Cancel;
            }
            return cancel;
        }

        public event EventHandler Inserted;
        protected virtual void OnInserted()
        {
            // TODO: Do not pass a CancelEventArgs object here.
            if (Inserted != null) 
                Inserted(this, EventArgs.Empty);
        }
        #endregion

        #region Update

        /// <summary>
        /// Cannot call Update directly, update can only be done during a call to Save().
        /// </summary>
        /// <param name="cn"></param>
        protected abstract void UpdateModel(IQuery query);

        public event EventHandler<CancelEventArgs> Updating;
        protected virtual bool OnUpdating()
        {
            bool cancel = false;
            if (Updating != null)
            {
                CancelEventArgs args = new CancelEventArgs();
                Updating(this, args);
                cancel = args.Cancel;
            }
            return cancel;
        }

        public event EventHandler Updated;
        protected virtual void OnUpdated()
        {
            if (Updated != null)
                Updated(this, EventArgs.Empty);
        }
        #endregion

        #region Delete

        /// <summary>
        /// Should not call this method directly. It is called in the Persist method,
        /// as appropriate, as a result of calling Save().
        /// </summary>
        /// <param name="cn"></param>
        protected abstract void DeleteModel(IQuery query);

        public event EventHandler<DeleteEventArgs> Deleting;
        protected virtual bool OnDeleting()
        {
            bool cancel = false;
            if (Deleting != null)
            {
                DeleteEventArgs args = new DeleteEventArgs();
                Deleting(this, args);
                cancel = args.Cancel;
            }
            return cancel;
        }

        public event EventHandler Deleted;
        protected virtual void OnDeleted()
        {
            if (Deleted != null)
                Deleted(this, EventArgs.Empty);
        }

        /// <summary>
        /// Flags this item as deleted. Will be saved when it is saved next.
        /// </summary>
        public virtual void Delete() { Delete(false); }
        
        /// <summary>
        /// Flags this item as deleted. If Immediate is true Save() is called also.
        /// </summary>
        /// <param name="Immediate"></param>
        public virtual void Delete(bool immediate)
        {
            _isDeleted = true;
            if (immediate) 
                Save();
        }
        #endregion        

        #region Access Rules
        private static AccessRuleCollection _AccessRules = new AccessRuleCollection();

        /// <summary>
        /// This is a list of Access rules related to this Entity. These rules specify which roles can
        /// get/set values from various fields.
        /// </summary>
        public static AccessRuleCollection AccessRules
        {
            get { return _AccessRules; }
        }

        /// <summary>
        /// Check to see if this user can Get this field.
        /// </summary>
        /// <returns></returns>
        public virtual void VerifyCanGet(string field)
        {
            OnAccessing();
            if (!ModelBase.AccessRules.CanGetField(this.GetType(), field, Thread.CurrentPrincipal)) 
                throw new SecurityException(
                    string.Format(CultureInfo.InvariantCulture, "Security Exception, getting field: ", field));
            OnAccessed();
        }
        /// <summary>
        /// Check to see if this user can set this field.
        /// </summary>
        /// <returns></returns>
        public virtual void VerifyCanSet(string field)
        {
            OnAccessing();
            if (!ModelBase.AccessRules.CanSetField(this.GetType(), field, Thread.CurrentPrincipal)) 
                throw new SecurityException(string.Format(CultureInfo.InvariantCulture, "Security Exception, setting field: ", field));
            OnAccessed();
        }

        public event EventHandler Accessing;
        protected virtual void OnAccessing()
        {
            if (Accessing != null) 
                Accessing(this, EventArgs.Empty);
        }

        public event EventHandler Accessed;
        protected virtual void OnAccessed()
        {
            if(Accessing != null) 
                Accessed(this, EventArgs.Empty);
        }
        #endregion

        #region Authorization Rules
        private static AuthorizationRuleCollection _authorizationRules = new AuthorizationRuleCollection();
        
        /// <summary>
        /// This is a list of Authorization rules related to this Entity and
        /// all of the Roles the current users is a memeber of.
        /// </summary>
        public static AuthorizationRuleCollection AuthorizationRules
        {
            get { return _authorizationRules; }
        }

        /// <summary>
        /// Check to see if this user can Fetch this object.
        /// </summary>
        public virtual bool CanFetch
        {
            get
            {
                OnAuthorizing();
                bool canFetch = ModelBase.CanFetchModel(this.GetType());
                OnAuthorized();
                return canFetch;
            }
        }

        public static bool CanFetchModel(Type type)
        {
            return AuthorizationRules.CanFetch(type, Thread.CurrentPrincipal);
        }

        /// <summary>
        /// Check to see if this user can Insert this object.
        /// </summary>
        public virtual bool CanInsert
        {
            get
            {
                OnAuthorizing();
                bool canInsert = ModelBase.CanInsertModel(this.GetType());
                OnAuthorized();
                return canInsert;
            }
        }

        public static bool CanInsertModel(Type type)
        {
            return AuthorizationRules.CanInsert(type, Thread.CurrentPrincipal);
        }

        /// <summary>
        /// Check to see if this user can Update this object.
        /// </summary>
        public virtual bool CanUpdate
        {
            get
            {
                OnAuthorizing();
                bool canUpdate = ModelBase.CanUpdateModel(this.GetType());
                OnAuthorized();
                return canUpdate;
            }
        }

        public static bool CanUpdateModel(Type type)
        {
            return AuthorizationRules.CanUpdate(type, Thread.CurrentPrincipal);
        }

        /// <summary>
        /// Check to see if this user can Delete this object.
        /// </summary>
        public virtual bool CanDelete
        {
            get
            {
                OnAuthorizing();
                bool canDelete = ModelBase.CanDeleteModel(this.GetType());
                OnAuthorized();
                return canDelete;
            }
        }

        public static bool CanDeleteModel(Type type)
        {
            return AuthorizationRules.CanDelete(type, Thread.CurrentPrincipal);
        }

        public event EventHandler Authorizing;
        protected virtual void OnAuthorizing()
        {
            if (Authorizing != null) 
                Authorizing(this, EventArgs.Empty);
        }

        public event EventHandler Authorized;
        protected virtual void OnAuthorized()
        {
            if (Authorized != null) 
                Authorized(this, EventArgs.Empty);
        }
        #endregion

        #region Validation Rules / IsValid
        /// <summary>
        /// Validates the user properties of this obejct to make sure they are ok to 
        /// be saved. If any of the broken rules are of type ValidationRuleType.Error then this
        /// property will return false.
        /// </summary>
        [Bindable(false)]
        public bool IsValid
        {
            get
            {
                //Clears out existing broken rules and re-validates them.
                //if no rules are broken for this field then this key will not
                //exist in the Broken rules collection.
                lock (_brokenRules)
                    _brokenRules.Clear();

                OnValidating();
                ValidateFields();
                OnValidated();

                bool valid = true;
                foreach (ValidationRule rule in BrokenRules)
                {
                    if (rule.RuleType == ValidationRuleType.Error)
                    {
                        valid = false;
                        break;
                    }
                }

                return valid;
            }
        }

        private static Collection<ValidationRule> _validationRules = new Collection<ValidationRule>();
        
        public static Collection<ValidationRule> ValidationRules
        {
            get { return _validationRules; }
        }

        private Collection<ValidationRule> _brokenRules = new Collection<ValidationRule>();
        
        public Collection<ValidationRule> BrokenRules
        {
            get { return _brokenRules; }
        }

        /// <summary>
        /// Call this method to validate the value of a given field against the provided Validation Rules.
        /// </summary>
        /// <param name="Field">The name of the field to validate.</param>
        /// <param name="Value">The value of the field to validate.</param>
        /// <returns>True if the value is valid.</returns>
        /// <example>ValidateField("SsnNumber", this.SsnNumber);</example>
        protected bool ValidateField(string field, object value)
        {
            bool valid = true;
            foreach (ValidationRule rule in ModelBase.ValidationRules)
            {
                if (rule.Type.Equals(this.GetType()) && rule.Field == field && !rule.Validate(value))
                {
                    valid = false;
                    lock (_brokenRules)
                        _brokenRules.Add(rule);
                }
            }
            return valid;
        }

        /// <summary>
        /// Implement this method to validate all fields. If any rules are broken they will be added
        /// to the BrokenRules collection.
        /// </summary>
        /// <example>
        /// protected override void Fields_Validate()
        /// {
        ///     ValidateField("SsnNumber", this.SsnNumber);
        ///     ValidateField("PhoneNumber", this.PhoneNumber);
        ///     ValidateField("Data", this.Data);
        /// }
        /// </example>
        protected abstract void ValidateFields();

        public event EventHandler Validating;
        protected virtual void OnValidating()
        {
            if (Validating != null) 
                Validating(this, EventArgs.Empty);
        }

        public event EventHandler Validated;
        protected virtual void OnValidated()
        {
            if (Validated != null) 
                Validated(this, EventArgs.Empty);
        }
        #endregion

        #region Load
        protected virtual void Load(ILoader loader)
        {
            OnLoading();
            LoadModel(loader);
            MarkOld();
            OnLoaded();
        }

        protected abstract void LoadModel(ILoader loader);

        public event EventHandler Loading;
        protected virtual void OnLoading()
        {
            if (Loading != null) 
                Loading(this, EventArgs.Empty);
        }

        public event EventHandler Loaded;
        protected virtual void OnLoaded()
        {
            if (Loaded != null) 
                Loaded(this, EventArgs.Empty);
        }

        #endregion

        #region Actions

        /// <summary>
        /// Attach handlers to events of this entity here.
        /// </summary>
        protected abstract void InitializeActions();

        // Supress warning about never using this event... it isn't supposed to ever be used.
        #pragma warning disable 0067
        /// <summary>
        /// This event is for attaching actions without the 'When' specified. It will force the C#
        /// compiler to insure that the attached method has been defined but it will never be called.
        /// </summary>
        protected event EventHandler Never;
        #pragma warning restore 0067

        #endregion

        #region Error

        public event EventHandler<EventArgs<Exception>> Error;
        
        protected virtual void OnError(Exception ex)
        {
            if (Error != null) 
                Error(this, new EventArgs<Exception>(ex));
        }
        #endregion

        #region Helpers
        protected IDatabaseProvider Provider
        {
            get { return (IDatabaseProvider)Settings.Instance.GetSetting(SettingsType.Provider, GetType()); }
        }
        #endregion
    }
}
